#include <iostream>
#include <vector>
#include <map>

#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>

static inline bool check_flag(int fd, int flag)
{
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        throw std::runtime_error("Failed to get file descriptor flags");
    }

    return flags & flag ? true : false;
}

static inline bool set_flag(int fd, int flag, bool set)
{
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        throw std::runtime_error("Failed to get file descriptor flags");
    }

    if (set) {
        if (flags & flag) {
            return false;   // not changed
        }
        flags |= flag;
    } else {
        if (!(flags & flag)) {
            return false;   // not changed
        }
        flags &= ~flag;
    }

    int ret = fcntl(fd, F_SETFL, flags);
    if (ret == -1) {
        throw std::runtime_error("Failed to set file descriptor flags");
    }

    return true;    // changed
}

class FileDesc {
public:
    FileDesc(const int fd, const std::string &filename)
        : m_fd(fd), m_name(filename)
    {}
    ~FileDesc() = default;

    bool set_nonblock(bool nonblock) {
        return set_flag(m_fd, O_NONBLOCK, nonblock);
    }

    bool get_nonblock(void) {
        return check_flag(m_fd, O_NONBLOCK);
    }

    std::string read(void);
    void write(const std::string &message);

    bool is_readable(void) const {
        return check_flag(m_fd, O_RDONLY | O_RDWR);
    }

    bool is_writable(void) const {
        return check_flag(m_fd, O_WRONLY | O_RDWR);
    }

    int get_fd(void) const {
        return m_fd;
    }

    std::string get_name(void) const {
        return m_name;
    }

protected:
    std::string m_name;
    int m_fd;

};


class MyFifo: public FileDesc {
public:
    MyFifo(const std::string &filename, int flags = O_RDWR, int mode=0)
        : FileDesc(0, filename)
    {
        int fd = open(m_name.c_str(), flags);
        if (fd == -1) {
            throw std::runtime_error("Failed to open FIFO");
        }

        m_fd = fd;
    }

    ~MyFifo()
    {
        if (m_fd > 0) {
            close(m_fd);
        }
    }
};

class MySocket: public FileDesc {
public:
    MySocket(int domain, int type, int protocol)
        : FileDesc(0, "socket")
    {
        m_fd = socket(domain, type, protocol);
        if (m_fd == -1) {
            throw std::runtime_error("Failed to create socket");
        }
        m_domain = domain;
    }

    ~MySocket() {
        if (m_fd > 0) {
            close(m_fd);
        }
    }

protected:
    int m_domain;

};

class MyServer: public MySocket {
public:
    MyServer(int domain, int type, int protocol)
        : MySocket(domain, type, protocol)
    {
    }
    ~MyServer() {
        for (auto &client: m_clients) {
            close(client.first);
        }
        MySocket::~MySocket();
    }

    int start(in_port_t sin_port, in_addr_t s_addr=INADDR_ANY) {
        sockaddr_in addr4 = {
            .sin_family = (sa_family_t)m_domain,
            .sin_port = htons(sin_port),
            .sin_addr = {s_addr = s_addr,},
        };

        if (bind(m_fd, reinterpret_cast<struct sockaddr *>(&addr4), sizeof(addr4)) == -1) {
            throw std::runtime_error("Failed to bind socket");
        }

        if (listen(m_fd, SOMAXCONN) == -1) {
            throw std::runtime_error("Failed to listen on socket");
        }

        return 0;
    }

    int accept(void) {
        int client_socket;
        struct sockaddr_in client_address;
        socklen_t client_address_length = sizeof(client_address);

        client_socket = ::accept(m_fd,
                reinterpret_cast<struct sockaddr *>(&client_address),
                &client_address_length);
        if (client_socket >= 0) {
            m_clients[client_socket] = client_address;
        }

        // FileDesc *clnt = new FileDesc(client_socket, "client");

        return client_socket;
    }

protected:
    std::map<int, struct sockaddr_in> m_clients;
};

class MyClient: public MySocket {
public:
    MyClient(int domain, int type, int protocol)
        : MySocket(domain, type, protocol)
    {
    }
    ~MyClient() = default;

    int connect(in_addr_t s_addr, in_port_t sin_port) {
        sockaddr_in addr4 = {
            .sin_family = (sa_family_t)m_domain,
            .sin_port = htons(sin_port),
            .sin_addr = {s_addr = s_addr,},
        };
        if (::connect(m_fd, reinterpret_cast<struct sockaddr *>(&addr4), sizeof(addr4)) == -1) {
            throw std::runtime_error("Failed to connect socket");
        }
        return 0;
    }

    int send(const void *buf, size_t len) {
        return ::send(m_fd, buf, len, 0);
    }

    int send(const std::string &mesg) {
        return ::send(m_fd, mesg.c_str(), mesg.length(), 0);
    }
};
